package IC.Semantic;

import IC.AST.*;
import IC.Tables.Symbols.*;
import IC.Tables.Types.*;

import java.util.*;


/**
 * Semantic checker
 */
public class SemanticChecker implements Visitor {
	private GlobalSymbolTable global;
	private boolean inStatic = false;
	private int loopCounter = 0;
	
	/**
	 * constructor	 * 
	 */
	public SemanticChecker(IC.Tables.Symbols.GlobalSymbolTable global){
		this.global = global;
	}

	
	/**
	 * Program Visitor
	 */
	public Object visit(Program program) {
		for(ICClass c: program.getClasses()){
			if (c.accept(this) == null) 
				return null;
		}
		return true;
	}

	
	/**
	 * ICClass Visitor
	 */
	public Object visit(ICClass icClass) {
		for(Method m: icClass.getMethods()){
			if (m.accept(this) == null) 
				return null;
		}
		return true;
	}

	
	/**
	 * Field visitor(never called) 
	 */
	public Object visit(Field field) {
		return true;
	}
	
	/**
	 * Method Visitor
	 */
	public Object methodVisitHelper(Method method){
		for(Statement s: method.getStatements()){
			if (s.accept(this) == null) 
				return null;
		}
		return true;
	}

	
	/**
	 * VirtualMethod visitor
	 */
	public Object visit(VirtualMethod method) {
		return methodVisitHelper(method);
	}

	
	/**
	 * StaticMethod visitor
	 */
	public Object visit(StaticMethod method) {
		inStatic = true;
		Object ret = methodVisitHelper(method);
		inStatic = false;
		return ret;
	}

	
	/**
	 * LibraryMethod visitor
	 */
	public Object visit(LibraryMethod method) {
		return methodVisitHelper(method);
	}

	
	/**
	 * Formal visitor (never called)
	 */
	public Object visit(Formal formal) {
		return true;
	}

	
	/**
	 * PrimitiveType visitor (never called)
	 */
	public Object visit(PrimitiveType type) {
		return true;
	}

	
	 /**
	  * UserType visitor (never called)
	  */
	public Object visit(UserType type) {
		return true;
	}

	
	/**
	 * Assignment visitor
	 */
	public Object visit(Assignment assignment) {
		IC.Tables.Types.Type locationType = (IC.Tables.Types.Type) assignment.getVariable().accept(this);
		if (locationType == null) 
			return null;
		
		IC.Tables.Types.Type assignmentType = (IC.Tables.Types.Type) assignment.getAssignment().accept(this);
		if (assignmentType == null)
			return null;
		
		if (!assignmentType.subtypeOf(locationType)){
			System.err.println(new SemanticError("type mismatch, expected: "+locationType.getName(),
					assignment.getLine(),
					assignmentType.getName()));
			return null;
		}
		
		return true;
	}

	
	/**
	 * CallStatement visitor
	 */
	public Object visit(CallStatement callStatement) {
		if (callStatement.getCall().accept(this) == null) 
			return null;
		return true;
	}

	
	/**
	 * returnStatement visitor
	 */
	public Object visit(Return returnStatement) {
		IC.Tables.Types.Type returnedValueType = null; 
		if (returnStatement.hasValue()){
			returnedValueType = (IC.Tables.Types.Type) returnStatement.getValue().accept(this);
			if (returnedValueType == null) 
				return null;
		} else try{
			returnedValueType = TypeTable.getType("void");
		} catch(SemanticError se){		} 
		
		try{
			IC.Tables.Types.Type returnType = ((BlockSymbolTable) returnStatement.getEnclosingScope()).getVarSymbolRecursive("_ret").getType();
			if (!returnedValueType.subtypeOf(returnType)){
				System.err.println(new SemanticError("type mismatch, expected: "+returnType.getName(),
						returnStatement.getLine(),
						returnedValueType.getName()));
				return null;
			}
		} catch (SemanticError se){}
			
		
		return true;
	}

	
	/**
	 * If visitor
	 */
	public Object visit(If ifStatement) {
		IC.Tables.Types.Type conditionType = (IC.Tables.Types.Type) ifStatement.getCondition().accept(this);
		if (conditionType == null)
			return null;
		
		try{
			if (!conditionType.subtypeOf(TypeTable.getType("boolean"))){
				System.err.println(new SemanticError("In if statement the condition must be boolean",
						ifStatement.getCondition().getLine(),
						conditionType.getName()));
				return null;
			}
		} catch (SemanticError se){}
				
		
		if (ifStatement.getOperation().accept(this) == null) 
			return null;
		if (ifStatement.hasElse()){
			if (ifStatement.getElseOperation().accept(this) == null) 
				return null;
		}
		
		return true;
	}

	
	/**
	 * While visitor
	 */
	public Object visit(While whileStatement) {
		IC.Tables.Types.Type conditionType = (IC.Tables.Types.Type) whileStatement.getCondition().accept(this);
		if (conditionType == null)
			return null;
		
		try{
			if (!conditionType.subtypeOf(TypeTable.getType("boolean"))){
				System.err.println(new SemanticError("In while statement the condition must be boolean",
						whileStatement.getCondition().getLine(),
						conditionType.getName()));
				return null;
			}
		} catch (SemanticError se){}
		
		
		loopCounter++;
		if (whileStatement.getOperation().accept(this) == null) {
			loopCounter--;
			return null;
		}
		loopCounter--;
		
		return true;
	}

	
	/**
	 * Break visitor
	 */
	public Object visit(Break breakStatement) {
		if (loopCounter == 0){
			System.err.println(new SemanticError("Break statement must be inside a loop",
					breakStatement.getLine(),
					"break"));
			return null;
		}
		
		return true;
	}

	
	/**
	 * Continue visitor
	 */
	public Object visit(Continue continueStatement) {
		if (loopCounter == 0){
			System.err.println(new SemanticError("Continue statement must be inside a loop",
					continueStatement.getLine(),
					"continue"));
			return null;
		}

		return true;
	}

	
	/**
	 * StatementsBlock visitor
	 */
	public Object visit(StatementsBlock statementsBlock) {
		for(Statement s: statementsBlock.getStatements()){
			if (s.accept(this) == null)
				return null;
		}
		return true;
	}

	
	/**
	 * LocalVariable visitor
	 */
	public Object visit(LocalVariable localVariable) {
		if (localVariable.hasInitValue()){
			IC.Tables.Types.Type initValueType = (IC.Tables.Types.Type) localVariable.getInitValue().accept(this);
			if (initValueType == null)
				return null;			
			try{
				IC.Tables.Types.Type localVariableType = ((BlockSymbolTable) localVariable.getEnclosingScope()).getVarSymbol(localVariable.getName()).getType();
				if (!initValueType.subtypeOf(localVariableType)){
					System.err.println(new SemanticError("type mismatch, expected: "+localVariableType.getName(),
							localVariable.getLine(),
							initValueType.getName()));
					return null;
				}
			} catch (SemanticError se){}
		}
		
		return true;
	}

	
	/**
	 * VariableLocation visitor
	 */
	public Object visit(VariableLocation location) {
		if (location.isExternal()){
			IC.Tables.Types.Type locationType = (IC.Tables.Types.Type) location.getLocation().accept(this);
			if (locationType == null) 
				return null;
			try{
				TypeTable.getClassType(locationType.getName());
				IC.Tables.Symbols.ClassSymbolTable cst = this.global.getClassSymbolTableRecursive(locationType.getName());
				try{
					IC.Tables.Symbols.FieldSymbol fs = cst.getFieldSymbolRecursive(location.getName());
					return fs.getType();
				} catch(SemanticError se){ 
					se.setLine(location.getLine());
					System.err.println(se);
					return null;
				}
			} catch(SemanticError se){ 
				System.err.println(new SemanticError("location type "+locationType.getName()+" doesn't have a field",
						location.getLine(),
						location.getName()));
				return null;
			}
		} else { 
			try{
				IC.Tables.Types.Type thisLocationType = ((BlockSymbolTable) location.getEnclosingScope()).getVarSymbolRecursive(location.getName()).getType();
				return thisLocationType;
			} catch(SemanticError se){ 
				se.setLine(location.getLine());
				System.err.println(se);
				return null;
			}
		}
	}

	
	/**
	 * ArrayLocation visitor
	 */
	public Object visit(ArrayLocation location) {
		IC.Tables.Types.ArrayType arrayType = (IC.Tables.Types.ArrayType) location.getArray().accept(this);
		if (arrayType == null)
			return null;
		IC.Tables.Types.Type indexType = (IC.Tables.Types.Type) location.getIndex().accept(this);
		if (indexType == null)
			return null;
		try{
			if (!indexType.subtypeOf(TypeTable.getType("int"))){
				System.err.println(new SemanticError("Array index must integer",
						location.getLine(),
						arrayType.getName()));
				return null;
			}
		}catch(SemanticError se){}
		
		return arrayType.getElemType();
	}

	
	/**
	 * StaticCall visitor
	 */
	public Object visit(StaticCall call) {
		IC.Tables.Symbols.ClassSymbolTable cst = global.getClassSymbolTableRecursive(call.getClassName());
		if (cst == null){ 
			System.err.println(new SemanticError("Class not found",
					call.getLine(),
					call.getClassName()));
			return null;
		}
		try{
			IC.Tables.Symbols.MethodSymbol ms = cst.getMethodSymbolRecursive(call.getName());
			if (!ms.isStatic()){
				System.err.println(new SemanticError("Method must be static",
						call.getLine(),
						call.getName()));
				return null;
			}
			Iterator<IC.Tables.Types.Type> methodArgsTypeIter = ((IC.Tables.Types.MethodType) ms.getType()).getParamTypes().iterator();
			for(Expression arg: call.getArguments()){
				IC.Tables.Types.Type argType = (IC.Tables.Types.Type) arg.accept(this);
				
				if (argType == null) 
					return null;
				
				if (!argType.subtypeOf(methodArgsTypeIter.next())){ 
					System.err.println(new SemanticError("Argument type is wrong",
							call.getLine(),
							argType.getName()));
					return null;
				}
			}
			if (methodArgsTypeIter.hasNext()){
				System.err.println(new SemanticError("Missing arguments",
						call.getLine(),
						call.getName()));
				return null;
			}
			
			return ((IC.Tables.Types.MethodType) ms.getType()).getReturnType();
		}catch (SemanticError se) { 
			se.setLine(call.getLine());
			System.err.println(se);
			return null;
		}catch (NoSuchElementException nsee){ 
			System.err.println(new SemanticError("Too many arguments",
					call.getLine(),
					call.getName()));
			return null;
		}
	}

	
	/**
	 * VirtualCall visitor
	 */
	public Object visit(VirtualCall call) {
		
		IC.Tables.Symbols.ClassSymbolTable cst = null;
		
		if (call.isExternal()){
			IC.Tables.Types.Type locType = (IC.Tables.Types.Type) call.getLocation().accept(this); 
			if (locType == null)
				return null; 
			
			cst = global.getClassSymbolTableRecursive(locType.getName());
			if (cst == null){ 
				System.err.println(new SemanticError("Location is not of a user defined type",
						call.getLine(),
						locType.getName()));
				return null;
			}
		}else { 
			cst = ((BlockSymbolTable)call.getEnclosingScope()).getEnclosingClassSymbolTable();
			if (inStatic){
				System.err.println(new SemanticError("Can't call a local virtual method from a static scope",
						call.getLine(),
						call.getName()));
				return null;
			}
		}
		
		MethodSymbol ms = null;
		try{
			ms = cst.getMethodSymbolRecursive(call.getName());
		} catch (SemanticError se) {
			se.setLine(call.getLine());
			System.err.println(se);
			return null;
		}
		
		if (ms.isStatic()){
			System.err.println(new SemanticError("Can't call a static method virtually",
					call.getLine(),
					call.getName()));
			return null;
		}
		Iterator<IC.Tables.Types.Type> methodArgsTypeIter = ((IC.Tables.Types.MethodType) ms.getType()).getParamTypes().iterator();
		for(Expression arg: call.getArguments()){
			IC.Tables.Types.Type argType = (IC.Tables.Types.Type) arg.accept(this);
			
			if (argType == null) return null;
			try{
				if (!argType.subtypeOf(methodArgsTypeIter.next())){ 
					System.err.println(new SemanticError("Wrong argument type passed to method",
							call.getLine(),
							argType.getName()));
					return null;
				}
			}catch (NoSuchElementException nsee){
				System.err.println(new SemanticError("Too many arguments passed to method",
						call.getLine(),
						call.getName()));
				return null;
			}
		}
		
		if (methodArgsTypeIter.hasNext()){
			System.err.println(new SemanticError("Too few arguments passed to method",
					call.getLine(),
					call.getName()));
			return null;
		}

		return ((IC.Tables.Types.MethodType) ms.getType()).getReturnType();
	}

	
	
	/**
	 *This visitor
	 */
	public Object visit(This thisExpression) {
		if (inStatic) {
			System.err.println(new SemanticError("'this' is illegal in a static method",
					thisExpression.getLine(),
					"this"));
			return null;
		}
		return ((BlockSymbolTable) thisExpression.getEnclosingScope()).getEnclosingClassSymbolTable().getMySymbol().getType();
	}

	
	/**
	 * NewClass visitor
	 */
	public Object visit(NewClass newClass) {
		IC.Tables.Types.ClassType ct = null;
		try{
			ct = IC.Tables.Types.TypeTable.getClassType(newClass.getName());
		}catch (SemanticError se){ 
			se.setLine(newClass.getLine());
			System.err.println(se);
			return null;
		}
		
		return ct;
	}

	
	/**
	 * NewArray visitor
	 */
	public Object visit(NewArray newArray) {
		IC.Tables.Types.Type elemType = null;
		
		try {
			elemType = TypeTable.getType(newArray.getType().getFullName());
		}catch (SemanticError se){ 
			se.setLine(newArray.getLine());
			System.err.println(se);
			return null;
		}
		
		IC.Tables.Types.Type sizeType = (IC.Tables.Types.Type) newArray.getSize().accept(this);
		
		if (sizeType == null) 
			return null;
		try {
			if (!sizeType.subtypeOf(TypeTable.getType("int"))){
				System.err.println(new SemanticError("Array size must be int type",
						newArray.getLine(),
						sizeType.getName()));
				return null;
			}
		} catch (SemanticError se) {}
		
		try{
			return TypeTable.getType(elemType.getName()+"[]");
		}catch (SemanticError se) {}
		
		return null;
	}

	
	/**
	 * Length visitor
	 */
	public Object visit(Length length) {
		IC.Tables.Types.Type arrType = (IC.Tables.Types.Type) length.getArray().accept(this);
		
		if (arrType == null) 
			return null;
		
		if (!arrType.getName().endsWith("[]")){ 
			System.err.println(new SemanticError("Not of array type",
					length.getLine(),
					arrType.getName()));
			return null;			
		}
				
		try {
			return TypeTable.getType("int");
		} catch (SemanticError se) {}
		
		return null;
	}

	
	/**
	 * MathBinaryOp visitor
	 */
	public Object visit(MathBinaryOp binaryOp) {
		IC.Tables.Types.Type op1Type = (IC.Tables.Types.Type) binaryOp.getFirstOperand().accept(this);
		IC.Tables.Types.Type op2Type = (IC.Tables.Types.Type) binaryOp.getSecondOperand().accept(this);
		if ((op1Type == null) || (op2Type == null))
			return null;
		if (op1Type != op2Type){ 
			System.err.println(new SemanticError("Math operation must be on same types",
					binaryOp.getLine(),
					binaryOp.getOperator().getOperatorString()));
			return null;
		}

		if (binaryOp.getOperator() != IC.BinaryOps.PLUS){
			try{
				if (!op1Type.subtypeOf(TypeTable.getType("int"))){
					System.err.println(new SemanticError("Math operation on a non int type",
							binaryOp.getLine(),
							op1Type.getName())); 
					return null;
				}
			} catch (SemanticError se){}
		}else{
			try{
				if (!op1Type.subtypeOf(TypeTable.getType("int")) && !op1Type.subtypeOf(TypeTable.getType("string"))){
					System.err.println(new SemanticError("Plus operation can be performed only on int or string",
							binaryOp.getLine(),
							op1Type.getName()));
					return null;
				}
			}catch (SemanticError se){}
		}
		
		return op1Type;
	}

	
	/**
	 * LogicalBinaryOp Visitor 
	 */
	public Object visit(LogicalBinaryOp binaryOp) {
		IC.Tables.Types.Type op1Type = (IC.Tables.Types.Type) binaryOp.getFirstOperand().accept(this);
		IC.Tables.Types.Type op2Type = (IC.Tables.Types.Type) binaryOp.getSecondOperand().accept(this);
		if ((op1Type == null) || (op2Type == null))
			return null;
		if (!op1Type.subtypeOf(op2Type) && !op2Type.subtypeOf(op1Type)){ 
			if (binaryOp.getOperator() == IC.BinaryOps.LAND || 
					binaryOp.getOperator() == IC.BinaryOps.LOR){ 
				System.err.println(new SemanticError("Logical operation must be performed on boolean types",
						binaryOp.getLine(),
						binaryOp.getOperator().getOperatorString()));
				return null;
			} else if (binaryOp.getOperator() == IC.BinaryOps.EQUAL || 
					binaryOp.getOperator() == IC.BinaryOps.NEQUAL) { 
				System.err.println(new SemanticError("Cannot compare foreign types",
						binaryOp.getLine(),
						binaryOp.getOperator().getOperatorString()));
				return null;				
			}else { // operator is one of "<=",">=","<",">"
				System.err.println(new SemanticError("Values must be int",
						binaryOp.getLine(),
						binaryOp.getOperator().getOperatorString()));
				return null;
			}
		}
		
		if ((binaryOp.getOperator() == IC.BinaryOps.LAND) ||
				(binaryOp.getOperator() == IC.BinaryOps.LOR)){
			try{
				if (!op1Type.subtypeOf(TypeTable.getType("boolean"))){
					System.err.println(new SemanticError("Logical operation on non boolean values",
							binaryOp.getLine(),
							op1Type.getName()));
					return null;
				}
			} catch (SemanticError se){}
		} else if (binaryOp.getOperator() != IC.BinaryOps.EQUAL && 
				binaryOp.getOperator() != IC.BinaryOps.NEQUAL) {
			try{
				if (!op1Type.subtypeOf(TypeTable.getType("int"))){
					System.err.println(new SemanticError("Comparing non int values",
							binaryOp.getLine(),
							op1Type.getName()));
					return null;
				}
			} catch (SemanticError se){}
		}
		
		// types are legal. return boolean type.
		IC.Tables.Types.Type ret = null;
		try{
			ret = TypeTable.getType("boolean");
		}catch (SemanticError se){}
		
		return ret;
	}

	
	/**
	 * a Visitor for MathUnaryOp - only one math unary operation - unary minus. 
	 * checks that the operand is of type int.
	 * returns type int.
	 */
	public Object visit(MathUnaryOp unaryOp) {
		IC.Tables.Types.Type opType = (IC.Tables.Types.Type) unaryOp.getOperand().accept(this);
		if (opType == null) return null;
		
		try{
			if (!opType.subtypeOf(TypeTable.getType("int"))){// opType is not an integer
				System.err.println(new SemanticError("Mathematical unary operation on a non int type",
						unaryOp.getLine(),
						opType.getName()));
				return null;
			}
		}catch  (SemanticError se){System.err.println("*** BUG: DefTypeCheckingVisitor, MathUnaryOP visitor");} // will never get here
		return opType; // int
	}

	
	/**
	 * a Visitor for LogicalUnaryOp - only one logic unary operation - unary logical negation. 
	 * checks that the operand is of type boolean.
	 * returns type boolean.
	 */
	public Object visit(LogicalUnaryOp unaryOp) {
		IC.Tables.Types.Type opType = (IC.Tables.Types.Type) unaryOp.getOperand().accept(this);
		if (opType == null) return null;
		
		try{
			if (!opType.subtypeOf(TypeTable.getType("boolean"))){// opType is not a boolean
				System.err.println(new SemanticError("Logical unary operation on a non boolean type",
						unaryOp.getLine(),
						opType.getName()));
				return null;
			}
		}catch  (SemanticError se){System.err.println("*** BUG: DefTypeCheckingVisitor, LogicalUnaryOP visitor");} // will never get here
		return opType; // boolean
	}

	
	/**
	 * Literal visitor:
	 * returns the type of the literal
	 */
	public Object visit(Literal literal) {
		IC.LiteralTypes type = literal.getType();
		try{
			// return the corresponding type of the literal
			switch (type){
			case STRING: return TypeTable.getType("string");
			case INTEGER: return TypeTable.getType("int");
			case TRUE: return TypeTable.getType("boolean");
			case FALSE: return TypeTable.getType("boolean");
			case NULL: return TypeTable.getType("null");
			}
		}catch(SemanticError se){System.err.println("*** BUG: DefTypeCheckingVisitor, Literal visitor");} // will never get here
		return null;
	}

	
	/**
	 * ExpressionBlock visitor:
	 * - recursive call to expression
	 * returns null if encountered an error, and the type of the expression otherwise 
	 */
	public Object visit(ExpressionBlock expressionBlock) {
		return (IC.Tables.Types.Type) expressionBlock.getExpression().accept(this);  // will return null if encounters an error
	}

}
